16. Lab III: Solution

Solution: Build a Dog GraphQL API - Queries & Exceptions

Below, we'll walk through each step of the lab and look at one potential way to implement the lab. Even if you get stuck, you should always first try to work through the lab without the solution before coming here, so that you can best learn the related skills and be ready for the project at the end of the course.

Step 1: Create resolvers that implement GraphQLQueryResolver.

  • The query resolver should match the operations in the GraphQL schema.
    • Query
    • findDogBreeds
    • findDogBreedById
    • findAllDogNames
    • Mutation
    • deleteDogBreed
    • updateDogName

First, create a resolver package and then add a new class Query that implements GraphQLQueryResolver. We just need to add the queries we put in dog.graphqls earlier.

I already added some of the necessary error handling in findDogById for Step 2 here, but it's essentially the same for this file as what you saw for the REST API.

package com.udacity.DogGraphQL.resolver;

import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.udacity.DogGraphQL.entity.Dog;
import com.udacity.DogGraphQL.exception.DogNotFoundException;
import com.udacity.DogGraphQL.repository.DogRepository;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class Query implements GraphQLQueryResolver {
    private DogRepository dogRepository;

    public Query(DogRepository dogRepository) {
        this.dogRepository = dogRepository;
    }

    public Iterable<Dog> findAllDogs() {
        return dogRepository.findAll();
    }

    public Dog findDogById(Long id) {
        Optional<Dog> optionalDog = dogRepository.findById(id);
        if (optionalDog.isPresent()) {
            return optionalDog.get();
        } else {
            throw new DogNotFoundException("Dog Not Found", id);
        }
    }
}

Next up are the mutators. Create a mutator package and then add a new class Mutation that implements GraphQLMutationResolver.

In these mutations, I am first using the findAll() query from the DogRepository, then processing to get the relevant entry (if available), and perform the requested operation. There are lots of ways to do this - you could instead add some additional queries to DogRepository to help, querying for just a single Dog by ID, for instance.

As with the queries, I've gone ahead and added the exception handling for Step 2 already for simplicity. The one for deleteDogBreed is not required in this exercise, but probably helpful for a user.

package com.udacity.DogGraphQL.mutator;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.udacity.DogGraphQL.entity.Dog;
import com.udacity.DogGraphQL.exception.BreedNotFoundException;
import com.udacity.DogGraphQL.exception.DogNotFoundException;
import com.udacity.DogGraphQL.repository.DogRepository;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class Mutation implements GraphQLMutationResolver {
    private DogRepository dogRepository;

    public Mutation(DogRepository dogRepository) {
        this.dogRepository = dogRepository;
    }

    public boolean deleteDogBreed(String breed) {
        boolean deleted = false;
        Iterable<Dog> allDogs = dogRepository.findAll();
        // Loop through all dogs to check their breed
        for (Dog d:allDogs) {
           if (d.getBreed().equals(breed)) {
               // Delete if the breed is found
               dogRepository.delete(d);
               deleted = true;
           }
        }
        // Throw an exception if the breed doesn't exist
        if (!deleted) {
            throw new BreedNotFoundException("Breed Not Found", breed);
        }
        return deleted;
    }

    public Dog updateDogName(String newName, Long id) {
        Optional<Dog> optionalDog = dogRepository.findById(id);

        if (optionalDog.isPresent()) {
            Dog dog = optionalDog.get();
            // Set the new name and save the updated dog
            dog.setName(newName);
            dogRepository.save(dog);
            return dog;
        } else {
            throw new DogNotFoundException("Dog Not Found", id);
        }
    }
}

Step 2: Make sure errors are handled appropriately.

  • If an id is requested that doesn’t exist, appropriately handle the error

Some of this has been handled in the above for the Query and Mutation. You might be tempted to fully re-use your code for DogNotFoundException from earlier, but we need a few changes for it to work properly with GraphQL (note that if you were also adding on a separate REST API using a service and controller, you may want to use separate exception handling for it). This time, you'll want to have it implement a GraphQLError, and no longer use the @ResponseStatus annotation we used with the REST API.

package com.udacity.DogGraphQL.exception;

import graphql.ErrorType;
import graphql.GraphQLError;
import graphql.language.SourceLocation;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class DogNotFoundException extends RuntimeException implements GraphQLError {

    private Map<String, Object> extensions = new HashMap<>();

    public DogNotFoundException(String message, Long invalidDogId) {
        super(message);
        extensions.put("invalidDogId", invalidDogId);
    }

    @Override
    public List<SourceLocation> getLocations() {
        return null;
    }

    @Override
    public Map<String, Object> getExtensions() {
        return extensions;
    }

    @Override
    public ErrorType getErrorType() {
        return ErrorType.DataFetchingException;
    }
}

If you are also wanting to implement the BreedNotFoundException, you can essentially just slightly alter the DogNotFoundException to do so. You could of course also potentially combine these into one exception file if you get a little more creative with the Exception itself.

package com.udacity.DogGraphQL.exception;

import graphql.ErrorType;
import graphql.GraphQLError;
import graphql.language.SourceLocation;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BreedNotFoundException extends RuntimeException implements GraphQLError {

    private Map<String, Object> extensions = new HashMap<>();

    public BreedNotFoundException(String message, String invalidBreed) {
        super(message);
        extensions.put("invalidBreedId", invalidBreed);
    }

    @Override
    public List<SourceLocation> getLocations() {
        return null;
    }

    @Override
    public Map<String, Object> getExtensions() {
        return extensions;
    }

    @Override
    public ErrorType getErrorType() {
        return ErrorType.DataFetchingException;
    }
}